package com.ck.distributedLock.service;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
import com.ck.common.utils.id.UUID;
import com.ck.common.utils.txt.TextUtil;
import com.ck.distributedLock.entity.LockEntity;
import com.ck.distributedLock.entity.UniqueLockEntity;
import com.ck.distributedLock.mapper.LockMapper;
import com.ck.distributedLock.mapper.UniqueLockMapper;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.dao.DataAccessException;
import org.springframework.data.redis.core.RedisOperations;
import org.springframework.data.redis.core.SessionCallback;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Service;

import java.util.Collections;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;

/**
 * @author ck
 * @date 2023/7/5 17:14
 */

@Service
@Slf4j
//@Scope(value = "prototype",proxyMode = ScopedProxyMode.TARGET_CLASS) /**多例模式(加锁也失效)*/
public class LockService {

    @Autowired
    private StringRedisTemplate stringRedisTemplate;
    @Autowired
    private LockMapper lockMapper;

    @Autowired
    private UniqueLockMapper uniqueLockMapper;

    @Autowired
    private RedissonClient redissonClient;
    private ReentrantLock reentrantLock= new ReentrantLock();

    /**
     * jvm锁(ReentrantLock、synchronized)失效场景
     * 1、spring默认是单例模式，如果是多例模式(prototype)，锁失效(每次调用service里的方法都会重新创建一个service对象)，synchronized锁的是对象，每次创建的对象都不一样，所以锁失效
     * 如果锁类，又因为synchronized是非公平锁，线程直接抢占资源是随机的，例如秒杀场景是不适用的，一般业务上禁止使用类锁(lock锁没有类锁)
     *
     * 2、加事务会导致锁失效(@Transactional是通过Aop实现的，例如两个线程同时进来，线程A获取锁，线程B等待，通过
     * Aop开启事务begin，线程A执行代码逻辑后释放锁，此时还没有commit，线程B获取锁，执行代码逻辑，此时线程B获取的数据和线程A获取的数据是一样的，导致锁失效)
     * mysql事务默认级别(读已提交)：一个事务只能读取到另一个事务已经提交的数据，不能读取到另一个事务未提交的数据
     *
     * 3、集群部署，锁失效(相当于是多例模式了，服务都不一样，锁的对象当然也不一样)
     *
     * 使用mysql的悲观锁也可以解决高并发问题  (select ...for update)
     * 产生的问题：
     * 1、性能问题
     * 2、死锁问题(加锁顺序不一致)
     */
    //@Transactional(加事务后锁失效)
    public /**synchronized*/ void updateCount1() {
        reentrantLock.lock();
        try {
            LockEntity lockEntity = lockMapper.selectOne(new QueryWrapper<LockEntity>().eq("product_id", 1001));
            if (lockEntity!=null && lockEntity.getCount()>0){
                lockEntity.setCount(lockEntity.getCount()-1);
                lockMapper.updateById(lockEntity);
                System.out.println(Thread.currentThread().getName()+"库存余量:"+lockEntity.getCount());
            }
            else {
                throw new RuntimeException("库存不足");
            }
        }
        catch (Exception e) {
            throw new RuntimeException(e);
        }
        finally {
            reentrantLock.unlock();
        }

    }


    /**
     * 乐观锁存在的问题:
     * 1、高并发情况下性能极低
     * 2、ABA问题
     * 3、读写分离的情况下导致乐观锁不可靠
     * @throws InterruptedException
     */
    public void updateCount2() throws InterruptedException {
        //查询库存
        List<LockEntity> lockEntities = lockMapper.selectList(new QueryWrapper<LockEntity>().eq("product_id", "1001"));

        //取第一个库存
        LockEntity lockEntity = lockEntities.get(0);

        if (lockEntity!=null && lockEntity.getCount()>0){
            //扣减库存
            lockEntity.setCount(lockEntity.getCount()-1);
            //查询版本号(利用version字段来判断是否被别的线程操作过)
            Integer version = lockEntity.getVersion();
            //执行一次写操作，版本号+1
            lockEntity.setVersion(version+1);

            /**
             * sql=update lock_test set count=count-1,version=version+1 where id=1 and version=上一次的查询的版本号
             * 如果更新失败，则说明被别的线程操作过更改了version，重试
             */
            if (lockMapper.update(lockEntity,new UpdateWrapper<LockEntity>().eq("id", lockEntity.getId()).eq("version", version)) == 0) {
//                log.info("update失败，重试");
//                Thread.sleep(200);
                //更新失败，重试
                updateCount2();
            }

            System.out.println(Thread.currentThread().getName()+"库存余量:"+lockEntity.getCount());
        }
    }


    /**
     * redis存储数据  高并发场景解决方法:
     * 1、jvm本地锁，与上面一样
     * 2、redis乐观锁:
     * watch(监听一个或多个key的值，如果在事务(exec)执行之前这个key的值被其他命令所改动，则取消事务执行)
     * multi(开启事务)
     * exec(执行事务)
     */
    /**
     * redis乐观锁实现
     */
    //TODO redis乐观锁实现还有问题，目前不知道问题在哪?
    public void updateCount3(){
        this.stringRedisTemplate.execute(new SessionCallback<Object>() {
            @Override
            public  Object execute(RedisOperations redisOperations) throws DataAccessException {
                //监听key值(watch命令)
                redisOperations.watch("stock");

                //查库存
                String stock = stringRedisTemplate.opsForValue().get("stock");
                //判断是否为空
                if (stock!=null && stock.length()!=0){
                    //判断库存是否大于0
                    int st = Integer.parseInt(stock);
                    if (st>0){
                        //开启事务(multi命令)
                        redisOperations.multi();

                        //扣减库存
                        stringRedisTemplate.opsForValue().set("stock",String.valueOf(--st));
                        //执行事务(exec命令)
                        List exec = redisOperations.exec();
                        if (exec == null || exec.size()==0){
                            try {
                                Thread.sleep(400);
                                updateCount3();
                            }
                            catch (InterruptedException e) {
                                throw new RuntimeException(e);
                            }
                        }
                        return exec;

                    }
                    else {
                        log.info("库存不足");
                    }
                }
                return null;
            }

        });
    }


    /**
     * redis分布式锁实现
     * 产生的几种问题：
     * 1、死锁: 要保证上锁和设置过期时间是原子操作(如果上锁后 服务宕机，此时还没设置过期时间，锁就永远不会释放了)
     * 2、误删锁(1):  A线程中上锁并设置过期时间完成以后后，系统出现了故障，导致锁到了过期时间并自动删除了，
     *              这时还没有执行手动释放锁的操作，这个时候B机器上锁成功，并去执行任务，任务还未执行完，A机器反应过来了，
     *              继续执行了手动释放锁的操作，把B线程上的锁给误删了
     * 解决方法(1):加uuid解决误删锁问题，唯一标识，在是否锁时先去判断value是否是自己的uuid，如果是则可以删除，否则不可以删除
     *
     * 3、误删锁(2):  A机器带锁id方式取锁、设置过期时间并执行完任务后，希望通过判断比较锁id之后去释放锁，判断通过后系统出现阻塞，
     *              阻塞到锁也到了过期时间自动释放了锁，这时还未进行手动释放锁操作，这个时候B机器上锁成功，并去执行任务，任务还未执行完，A机器反应过来了，
     *              继续执行了手动释放锁的操作，把B机器上的锁给误删了
     *解决方法(2):保证判断锁和释放锁的原子性：使用redis执行LUA脚本，保证一步执行判断锁和释放锁。
     *
     * 4、redisson框架实现分布式锁(解决上述的所有问题)
     */

    public void updateCount4(){
        //生成uuid
        String uuid = UUID.randomUUID().toString();
        //加分布式锁(redis的setnx命令)
        while (!this.stringRedisTemplate.opsForValue().setIfAbsent("lock", uuid, 3, TimeUnit.SECONDS)){
            try {
                //锁被当前线程占有，重试获取锁
                Thread.sleep(50);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
            //查库存
            try {
                String stock = stringRedisTemplate.opsForValue().get("stock");
                //判断是否为空
                if (stock != null && stock.length() != 0) {
                    //判断库存是否大于0
                    int st = Integer.parseInt(stock);
                    if (st > 0) {
                        //扣减库存
                        stringRedisTemplate.opsForValue().set("stock", String.valueOf(--st));
                    } else {
                        log.info("库存不足");
                    }
                }
            } catch (NumberFormatException e) {
                throw new RuntimeException(e);
            }
            finally {
                String luaScript = "if redis.call('get', KEYS[1]) == ARGV[1] "+
                        "then "+
                        "return redis.call('del', KEYS[1]) "+
                        "else "+
                        "return 0 "+
                        "end";
                //释放锁(判断是否是自己的锁)
//                if (uuid.equals(stringRedisTemplate.opsForValue().get("lock"))){
//                    stringRedisTemplate.delete("lock");
//                }
                this.stringRedisTemplate.execute(new DefaultRedisScript<>(luaScript, Long.class), Collections.singletonList("lock"), uuid);
            }
        }


    /**
     * 大Boss  redisson框架实现分布式锁
     */
    public void updateCount5(){
        //redisson加锁
        RLock lock = redissonClient.getLock("lock");
        lock.lock();  /**阻塞式等待,默认加锁时间30s */

        //查库存
        try {
            String stock = stringRedisTemplate.opsForValue().get("stock");
            //判断是否为空
            if (stock != null && stock.length() != 0) {
                //判断库存是否大于0
                int st = Integer.parseInt(stock);
                if (st > 0) {
                    //扣减库存
                    stringRedisTemplate.opsForValue().set("stock", String.valueOf(--st));
                } else {
                    log.info("库存不足");
                }
            }
        } catch (NumberFormatException e) {
            throw new RuntimeException(e);
        }
        finally {
            //释放锁
            lock.unlock();
        }
    }

    /**
     * 基于mysql的 唯一键索引的特性实现分布式锁(性能很差)
     */
    public void updateCount6()  {

        try {
            UniqueLockEntity  uniqueLockEntity = new UniqueLockEntity();
            uniqueLockEntity.setLockName("lock");
            //加锁
            uniqueLockMapper.insert(uniqueLockEntity);

            //查库存
            String stock = stringRedisTemplate.opsForValue().get("stock");
            //判断是否为空
            if (stock != null && stock.length() != 0) {
                //判断库存是否大于0
                int st = Integer.parseInt(stock);
                if (st > 0) {
                    //扣减库存
                    stringRedisTemplate.opsForValue().set("stock", String.valueOf(--st));
                } else {
                    log.info("库存不足");
                }
            }

            //解锁
            uniqueLockMapper.deleteById(uniqueLockEntity.getId());
        }
        catch (Exception e) {
          log.info("加锁失败",TextUtil.ex2Txt(e));

          //重试
            try {
                Thread.sleep(50);
                updateCount6();
            } catch (InterruptedException ex) {
                log.info("加锁失败",TextUtil.ex2Txt(e));
            }

        }
    }
}






